Dashboard page
The patient dashboard is the widget grid shown on the patient overview tab. A Dashboard config maps roles (or default) to a DashboardInstance — four widget lists (top, left, right, bottom) — and the Dashboards component renders each list as a sequence of widget components.
Sources:
src/components/Dashboard/contexts.tsxsrc/components/Dashboard/types.tssrc/components/Dashboard/index.tsxsrc/containers/PatientDetails/Dashboard/config.ts
Import:
import { Dashboards } from 'src/components/Dashboard';
import { PatientDashboardProvider, useDashboard } from 'src/components/Dashboard/contexts';
import type { Dashboard, DashboardInstance, WidgetInfo, WidgetProps, Query } from 'src/components/Dashboard/types';
How it's wired up
-
src/dashboard.config.tsbuilds the top-levelDashboardobject, currently just{ default: patientDashboardConfig }frompatientDashboardConfiginconfig.ts. -
main.tsxwraps the app in<PatientDashboardProvider dashboard={dashboard}>, putting that config onPatientDashboardContextfor the whole tree. -
PatientOverviewcallsuseDashboard()to read the resolvedDashboardInstance, then renders oneDashboardsper area:const patientDashboard = useDashboard();<S.Container><Dashboards widgets={patientDashboard.top} patient={patient} /><S.Cards><S.Column><Dashboards widgets={patientDashboard.left} patient={patient} /></S.Column><S.Column><Dashboards widgets={patientDashboard.right} patient={patient} /></S.Column></S.Cards><Dashboards widgets={patientDashboard.bottom} patient={patient} /></S.Container>
Dashboards itself just maps a WidgetInfo[] to mounted widget components, passing patient and the originating widgetInfo through:
export function Dashboards({ patient, widgets }: Props) {
return (
<>
{widgets.map((widgetInfo, index) => {
const WidgetComponent = widgetInfo.widget;
return <WidgetComponent key={index} patient={patient} widgetInfo={widgetInfo} />;
})}
</>
);
}
top and bottom render full-width in document order; left and right render as two columns between them — that's a layout convention of PatientOverview's styles, not something Dashboards enforces itself.
useDashboard and role-based dashboards
export interface Dashboard {
default: DashboardInstance;
}
export function useDashboard() {
const patientDashboard = useContext(PatientDashboardContext);
// TODO select dashboard based on the role
return patientDashboard.default;
}
Dashboard is keyed by role today only in shape — useDashboard always returns patientDashboard.default, with a TODO marking role-based selection as unimplemented. dashboard.config.ts already shows the intended extension point, commented out:
export const dashboard: Dashboard = {
default: patientDashboardConfig,
// [Role.Admin]: {},
// [Role.Practitioner]: {},
};
Until role-based selection lands, every signed-in user sees the same default dashboard regardless of role.
WidgetInfo and Query
export type DashboardAreas = 'top' | 'right' | 'left' | 'bottom';
export type DashboardInstance = Record<DashboardAreas, WidgetInfo[]>;
export interface WidgetInfo {
widget: React.FunctionComponent<WidgetProps>;
query?: Query;
}
export interface WidgetProps {
patient: Patient;
widgetInfo: WidgetInfo;
}
export interface ContainerProps {
patient: Patient;
widgetInfo: WidgetInfo;
}
export interface Query {
resourceType: FhirResource['resourceType'];
search: (patient: Patient) => SearchParams;
}
Each entry in a DashboardInstance area is a { widget, query? } pair:
widget— a component matchingWidgetProps({ patient, widgetInfo }, aliased asContainerPropsin widget containers).Dashboardsmounts it directly.query— optional. When present, it's the widget's contract for what to fetch: aresourceTypeand asearch(patient)function returning FHIR search params.queryis only data attached to the config entry — nothing inDashboardsoruseDashboardexecutes it. It's up to thewidgetcomponent to readwidgetInfo.queryand fetch accordingly (seeStandardCardContainerFabricandAppointmentCardContainerbelow).
A widget with no query fetches its own data internally, the way ViewChart-based widgets do.
patientDashboardConfig
src/containers/PatientDetails/Dashboard/config.ts is the DashboardInstance used as dashboard.default:
export const patientDashboardConfig: DashboardInstance = {
top: [
{
widget: AppointmentCardContainer,
query: {
resourceType: 'Appointment',
search: (patient: Patient) => ({
patient: patient.id,
status: ['arrived,booked'],
}),
},
},
{
widget: GeneralInformationDashboardContainer,
},
{
query: {
resourceType: 'Condition',
search: (patient: Patient) => ({
patient: patient.id,
_sort: ['-_recorded-date'],
_revinclude: ['Provenance:target'],
_count: 7,
}),
},
widget: StandardCardContainerFabric(prepareConditions),
},
// ...AllergyIntolerance, MedicationStatement, Immunization, Procedure follow the same shape
],
left: [],
right: [],
bottom: [
{
widget: CreatinineDashboardContainer,
},
],
};
This shows the three widget patterns in the codebase:
| Pattern | Example | query | Data fetching |
|---|---|---|---|
| Query-driven, generic card | StandardCardContainerFabric(prepareConditions) | Yes | useStandardCard runs query.search(patient) against query.resourceType, then formats the result with a PrepareFunction |
| Query-driven, bespoke widget | AppointmentCardContainer | Yes | Its own hook (useAppointmentCard) runs widgetInfo.query itself |
| Self-fetching widget | GeneralInformationDashboardContainer, CreatinineDashboardContainer | No | Widget (or the ViewChart it renders) fetches internally, ignoring widgetInfo.query |
Query-driven widgets: StandardCardContainerFabric
Most top-area cards (conditions, allergies, medications, immunizations, procedures) share one generic renderer built by StandardCardContainerFabric<T>(prepareFunction, cardProps?). It returns a ContainerProps component that:
- Requires
widgetInfo.query— renders an error<div>if missing. - Calls
useStandardCard(patient, query, prepareFunction), which runsgetFHIRResources(query.resourceType, { ...query.search(patient), _count: countNumber }), extracts the bundle, and callsprepareFunction(resources, bundle, total, to)to build anOverviewCard. - Renders
StandardCardwith the resulting card viaRenderRemoteData(spinner while loading).
A PrepareFunction turns raw FHIR resources into an OverviewCard:
export type PrepareFunction<T extends Resource> =
| ((resources: T[], bundle: Bundle<T>, total: number, to?: string) => OverviewCard<T>)
| ((resources: T[]) => OverviewCard<T[]>);
export interface OverviewCard<T extends Resource | Resource[]> {
title: string;
key: string;
icon: React.ReactNode;
data: T[];
total?: number;
columns: { key: string; title: string; render: (r: T) => React.ReactNode; width?: string | number }[];
getKey: (r: T) => string;
}
prepareConditions is one such function — given Condition[] and the search Bundle, it returns a title, icon, and column definitions (with render typically wrapping the value in LinkToEdit to open an edit questionnaire).
To add a new query-driven card of the same shape:
- Write a
PrepareFunction<T>for your resource type inprepare.tsx(or your own file). - Add an entry to
patientDashboardConfig(or a custom config) withwidget: StandardCardContainerFabric(yourPrepareFunction)and aquerydescribing the resource and search params.
Bespoke query-driven widgets: AppointmentCardContainer
When a widget's rendering doesn't fit the generic table shape (AppointmentCard renders custom cards, not StandardCard columns), it still follows the same query-driven contract but with its own hook instead of useStandardCard:
export function AppointmentCardContainer(props: ContainerProps) {
const { widgetInfo } = props;
if (!widgetInfo.query) {
return <div>Error: no query parameter for the widget.</div>;
}
return <AppointmentCardWrapper {...props} />;
}
function AppointmentCardWrapper({ patient, widgetInfo }: ContainerProps) {
const { response } = useAppointmentCard(patient, widgetInfo.query!);
return (
<RenderRemoteData remoteData={response} renderLoading={Spinner}>
{({ appointments }) => appointments.map((appointment, index) => (
<AppointmentCard key={index} appointment={appointment} />
))}
</RenderRemoteData>
);
}
Use this pattern — read widgetInfo.query in your own hook — when you need custom rendering but still want the resource type and search params to live declaratively in the dashboard config rather than hardcoded in the widget.
Self-fetching widgets
Widgets that don't need query fetch their own data and ignore widgetInfo.query entirely:
GeneralInformationDashboardContainercallsuseGeneralInformationDashboard(patient)internally and rendersGeneralInformationDashboard.CreatinineDashboardContainerforwardspatientto aViewChart-based component, which fetches rows from aViewDefinition/Libraryitself.
Prefer this pattern when the widget's data source isn't a single FHIR search (a ViewChart/SQL on FHIR chart, an aggregate, or a hook that composes several calls), since forcing it into query.search(patient) would just move the same logic into a function that has to run inside another hook anyway.
Adding a widget to the dashboard
- Build a component matching
WidgetProps/ContainerProps({ patient, widgetInfo }), reusingStandardCardContainerFabricif a generic card fits, or a bespoke container otherwise. - Add
{ widget, query? }to the appropriate area (top,left,right,bottom) ofpatientDashboardConfiginconfig.ts. - In a custom EMR build, override
src/containers/PatientDetails/Dashboard/config.ts(orsrc/dashboard.config.tsfor role-based dashboards) to add, remove, or reorder widgets without touching the base component code.
Related documentation
- ViewChart component — chart widgets backed by
ViewDefinition/Library, and how they slot intopatientDashboardConfig - Resource detail page — tabbed FHIR resource detail layout, another place
StandardCard-style widgets are used - Custom EMR build — project template and override points for
dashboard.config.ts